import { MessageStyle } from "@/hooks/message";
import axios, { AxiosRequestConfig } from "axios";
import { delay } from "..";
import { getGlobalEnv } from "../constants";
import {
  baseFieldConfig,
  baseResponseCodeConfig,
  FieldConfig,
  ResponseCodeConfig,
} from "./config";
import { catchException } from "./exception";
import { beforeRequest } from "./request";
import { transformResponse } from "./response";

export interface ZAxiosOption<D = any> {
  config: AxiosRequestConfig<D>;

  // ----- field config ----- //

  fieldConfig?: FieldConfig;

  // ----- request ----- //

  // if you need something likes '/v1'
  urlPrefix?: string;
  // with trailing params ${trailingTimeName}=<timestamp> for 'GET'
  // Caution, it breaks [http caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#overview).
  trailingTime?: boolean;
  // token based request
  carryToken?: boolean;

  // ----- response ---- //

  // By default, you would get ${response.data.result}, set true to get ${response.data} directly
  // in case of non-well-strutted ${response.data}, likes strings,
  // which means you won't benefit from the global catch for response errors.
  preserveResponseData?: boolean;
  // set true to return raw response without ${transformResponse}, it's useful when you need ${headers}
  preserveNativeResponse?: boolean;
  // how to display ${response.data.message}. By default, except for 'GET', operations have a <message> tip.
  messageStyle?: MessageStyle;
  // retry times to stop. Default 0 means no retry.
  // Note that's the number of extra request excepting the original one.
  retry?: number;
  // retry after how many milliseconds.
  retryDelay?: number;
  // basically to distinguish Success or Fail
  responseCodeConfig?: ResponseCodeConfig;
}

export function createAxios(globalOption: ZAxiosOption) {
  const { config, ...option } = globalOption;
  const instance = axios.create(config);

  return {
    // TODO: redo GET,POST,PUT,DELETE functions
    request: <R = unknown>(
      config: AxiosRequestConfig,
      localOption?: Omit<ZAxiosOption, "config">
    ) => {
      const finalOption = Object.assign(
        Object.create(null),
        option,
        localOption,
        // merge reference items
        {
          fieldConfig: Object.assign(
            Object.create(baseFieldConfig),
            option?.fieldConfig,
            localOption?.fieldConfig
          ),
          responseCodeConfig: Object.assign(
            Object.create(baseResponseCodeConfig),
            option?.responseCodeConfig,
            localOption?.responseCodeConfig
          ),
        }
      );

      const requestConfig = beforeRequest(config, finalOption);

      let retryCount = 0;
      let initPromise: any[] = [];
      function _request() {
        return new Promise<R>((resolve, reject) => {
          if (!initPromise.length) initPromise = [resolve, reject];

          instance
            .request(requestConfig)
            .then((response) => {
              const [key, res] = transformResponse<R>(response, finalOption);
              initPromise[key](res as R);
            })
            .catch(async (reason) => {
              const { retry = 0, retryDelay = 0 } = finalOption;
              if (retryCount++ < retry) {
                await delay(retryDelay);
                await _request();
                return;
              }
              catchException(reason);
              initPromise[1](reason);
            });
        });
      }
      return _request();
    },
  };
}

const { baseApiUrl } = getGlobalEnv();

/* use ${baseHttp} when there is only one service. Otherwise, customize extra services at `/api` */
const baseAxiosGlobalOption: ZAxiosOption = {
  config: {
    baseURL: baseApiUrl,
    timeout: 1000,
    headers: { "X-Custom-Header": "base api" },
  },
  trailingTime: true,
};

export const baseHttp = createAxios(baseAxiosGlobalOption);
